package com.tarena.lbs.marketing.web.service;

import com.alibaba.nacos.common.utils.CollectionUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tarena.lbs.attach.api.AttachApi;
import com.tarena.lbs.base.common.utils.Asserts;
import com.tarena.lbs.base.protocol.exception.BusinessException;
import com.tarena.lbs.base.protocol.pager.PageResult;
import com.tarena.lbs.basic.api.BasicApi;
import com.tarena.lbs.common.passport.enums.Roles;
import com.tarena.lbs.common.passport.principle.UserPrinciple;
import com.tarena.lbs.marketing.web.dao.repository.ActivityRepository;
import com.tarena.lbs.marketing.web.dao.repository.CouponCodeRepository;
import com.tarena.lbs.marketing.web.dao.repository.CouponRepository;
import com.tarena.lbs.marketing.web.dao.repository.UserCouponsRepository;
import com.tarena.lbs.marketing.web.thread.AuthenticationUtil;
import com.tarena.lbs.pojo.attach.dto.AttachQrDTO;
import com.tarena.lbs.pojo.attach.param.AttachQRParam;
import com.tarena.lbs.pojo.marketing.param.UserCouponsParam;
import com.tarena.lbs.pojo.marketing.po.ActivityPO;
import com.tarena.lbs.pojo.marketing.po.CouponCodePO;
import com.tarena.lbs.pojo.marketing.po.CouponPO;
import com.tarena.lbs.pojo.marketing.po.UserCouponsPO;
import com.tarena.lbs.pojo.marketing.query.UserCouponCodeQuery;
import com.tarena.lbs.pojo.marketing.query.UserCouponQuery;
import com.tarena.lbs.pojo.marketing.vo.UserCouponsVO;
import com.tarena.lbs.stock.api.StockApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class UserCouponsService {
    //活动查询 活动验证 用户领取记录对比
    @Autowired
    private ActivityRepository activityRepository;
    //优惠券查询 验证优惠券
    @Autowired
    private CouponRepository couponRepository;
    //创建优惠券 券码 修改券码状态
    @Autowired
    private CouponCodeRepository couponCodeRepository;
    //远程调用 basic 校验目标人群
    @DubboReference
    private BasicApi basicApi;
    //库存 查询 和扣减
    @DubboReference
    private StockApi stockApi;
    //领取记录 查询领取次数 新增领成功的记录
    @Autowired
    private UserCouponsRepository userCouponsRepository;

    //图片服务接口
    @DubboReference
    private AttachApi attachApi;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    public void getCoupon(UserCouponsParam param) throws BusinessException, ExecutionException, InterruptedException {
        //1.获取一个认证对象的userId 领取记录不可少的数据 获取的同时 检查一下认证状态
        Integer userId;
        if (param.getUserId()==null) {
            //userId在param参数中为空说明是前端http调用
            userId = checkAndGetUser();
        }else{
            userId=param.getUserId();
        }
        /*
        */
        //spring框架可以直接调用 从线程池 调用异步线程 执行并行的代码块
        CompletableFuture<ActivityPO> activityFuture = CompletableFuture.supplyAsync(() -> {
            try {
                return checkActivityThread(param, userId);
            } catch (Exception e) {
                log.error("异步方法检查活动 有异常", e);
            }
            return null;
        });
        CompletableFuture<CouponPO> couponFuture = CompletableFuture.supplyAsync(() -> {
            try {
                return checkCouponThread(param, userId);
            } catch (Exception e) {
                log.error("异步方法检查活动 有异常", e);
            }
            return null;
        });
        //需要主线程等待异步线程所有人 执行结束 将主线程阻塞一下
        CompletableFuture<Void> allFutrues = CompletableFuture.allOf(activityFuture, couponFuture);
        CouponPO couponPo = couponFuture.get();
        ActivityPO activityPo = activityFuture.get();
        //6.用户可以领取优惠券了 Spring框架 提供了切面上下文,如果代码本地调用可以通过
        /*UserCouponsService aopService = (UserCouponsService) AopContext.currentProxy();*/
       /* aopService.*/
        //添加分布式锁 setnx 属于string数据类型的命令 hset hget hkey hval hincrby HASH  lpush rpush lpop LIST
        //sadd sremove SET  zadd zrank SORTED-SET
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        //设置获取 所的key值  和value  key和争抢的资源业务有关 couponId value 随机值
        String lockKey="marketing:coupon:take:"+param.getCouponId();
        String lockValue=UUID.randomUUID().toString().replace("-","");
        Boolean tryLock=false;//抢锁结果
        int count=0;//统计当前抢锁次数
        //SETNX EXPIRE
        do{
            if (count>0) {
                log.info("多次抢锁 等待1秒");
                try{
                    //多次抢锁 沉睡1秒钟
                    Thread.sleep(1000);
                }catch (Exception e) {
                    log.error("线程睡眠被破坏了");
                }
            }
            tryLock = opsForValue.setIfAbsent(lockKey, lockValue, 5, TimeUnit.SECONDS);
            count++;
            log.info("抢锁完成,已经抢到:{}次",count);
        }while(!tryLock);//没抢到循环执行
        //出来 说明线程抢到锁了
        try{
            log.info("抢锁成功 执行业务逻辑");
            receiveCoupon(param, userId, activityPo, couponPo);
        }catch (Exception e){
            throw new BusinessException("-2",e.getMessage());
        }finally{
            //释放锁
            String value = opsForValue.get(lockKey);
            if (StringUtils.equals(value,lockValue)){
                log.info("验证锁的身份完成,可以删除");
                //当前这把锁的key值 是线程抢到的可以删除
                stringRedisTemplate.delete(lockKey);
            }
        }
    }

    private CouponPO checkCouponThread(UserCouponsParam param,Integer userId) throws BusinessException {
        //4.获取一个优惠券对象 并且校验优惠券有效性
        CouponPO couponPo=checkAndGetCoupon(param.getCouponId());
        //5.校验一下 当前用户 已经领取该优惠券是否超过 上限
        checkUsageLimit(userId,couponPo);
        return couponPo;
    }

    private ActivityPO checkActivityThread(UserCouponsParam param,Integer userId) throws BusinessException {
        //2.获取一个活动对象 并且校验活动有效性
        ActivityPO activityPo=checkAndGetActivity(param.getActivityId());
        //3.校验一下活动目标人群 和当前登录用户是否一致
        checkIsTargetConsumer(userId,activityPo.getTargetCustomer(),activityPo.getBusinessId());
        return activityPo;
    }

    //注意: 当前方法 是本地调用 写Transactional 不会生效
    public void receiveCoupon(UserCouponsParam param, Integer userId, ActivityPO activityPo, CouponPO couponPo) throws BusinessException {

        //1.创建一个券码数据 懒加载 初始状态是0 待领取
        CouponCodePO couponCodePo =initAndGetCouponCode(couponPo.getBusinessId(),couponPo.getId());
        //2.记录当前用户的领取信息 表示已领 user_coupons
        saveUserCoupons(userId,param,couponCodePo.getCouponCode(),couponPo);
        //3.领取成功 修改一下券码状态 为1 已领取
        updateCouponCodeReceived(couponCodePo.getId());
        //4.保证库存数据 和当前领取结果是一致的 扣减一下库存
        reduceStock(couponPo.getId());
    }

    private void updateCouponCodeReceived(Integer id) {
        //update coupon_code set status=1 where id=#{}
        CouponCodePO poParam=new CouponCodePO();
        poParam.setId(id);
        poParam.setStatus(1);
        couponCodeRepository.updateById(poParam);
    }

    private void saveUserCoupons(Integer userId, UserCouponsParam param, String couponCode, CouponPO couponPo) {
        //1.构建一个领取记录的对象 写入数据库
        UserCouponsPO po=new UserCouponsPO();
        //2.有一部分 userCoupons属性来自于couponPo
        BeanUtils.copyProperties(couponPo,po);//couponPo有个id 拷贝 会交给po使用
        po.setId(null);//如果不设置 每次用户领取记录都是优惠券的id
        po.setUserId(userId);
        po.setCouponId(couponPo.getId());
        po.setCouponCode(couponCode);
        po.setCreateAt(new Date());
        po.setUpdateAt(po.getCreateAt());
        po.setStatus(0);
        //手动补充一些数据库非空校验字段 领取渠道 短信 app消息 活动页面 店铺主页等 主要用来做数据统计
        po.setReceiveChannel(1);
        po.setActivityId(param.getActivityId());
        po.setShopId(param.getShopId());
        po.setCouponValue(couponPo.getDiscountValue());
        //用户领取记录的po实体对象封装完毕
        log.info("当前用户领取记录数据封装完毕:{}",po);
        userCouponsRepository.save(po);
    }

    private CouponCodePO initAndGetCouponCode(Integer businessId, Integer id) {
        //利用入参 构造一个CouponCodePO 通过仓储层写入数据库 返回po
        CouponCodePO po=new CouponCodePO();
        po.setBusinessId(businessId);
        po.setCouponId(id);
        po.setCreateAt(new Date());
        po.setUpdateAt(po.getCreateAt());
        po.setStatus(0);//0 待领取
        String couponCode= UUID.randomUUID().toString().replace("-","");
        po.setCouponCode(couponCode);
        couponCodeRepository.save(po);
        return po;
    }

    private void reduceStock(Integer couponId) throws BusinessException {
        //需要不需要考虑 领取优惠券 扣减数量总是1
        try{
            Boolean result = stockApi.reduceStock(couponId, 1);
        }catch (Exception e){
            log.error("优惠券远程调用失败",e);
            throw new BusinessException("-2","库存不够 失败");
            //监控 预警接口 普罗米修斯
        }
        //确实要验证返回结果 但是如果是失败,保证 必定不能是库存不够而失败
    }

    private void checkStockEnough(Integer couponId) throws BusinessException {
        //1.调用远程 查询剩余数量
        Integer couponNum = stockApi.getCouponNum(couponId);
        //2.断言 是否大于当前操作扣减的数量
        Asserts.isTrue(couponNum<=0,new BusinessException("-2","库存不足"));
    }

    private void checkUsageLimit(Integer userId, CouponPO couponPo) throws BusinessException {
        //1.获取优惠券 设定的每个用户领取优惠券上限
        Integer usageLimit = couponPo.getUsageLimit();//假设是5
        log.info("用户:{},领取优惠券:{},优惠券上限:{}",userId,couponPo.getId(),usageLimit);
        //2.查询当前用户 领取当前优惠券 已经领了几张 select count(0) from user_coupons
        //where user_id=#{} and coupon_id=#{}
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("user_id",userId);
        queryWrapper.eq("coupon_id",couponPo.getId());
        Long count=userCouponsRepository.count(queryWrapper);
        log.info("用户:{},领取优惠券:{},已经领取:{}",userId,couponPo.getId(),count);
        Asserts.isTrue(count>= usageLimit.longValue(),new BusinessException("-2","该用户已达领取上限"));
    }

    private CouponPO checkAndGetCoupon(Integer couponId) throws BusinessException {
        //1.先查询 优惠券的详情po
        CouponPO po=couponRepository.getById(couponId);
        //2. 断言空
        Asserts.isTrue(po==null,new BusinessException("-2","优惠券不存在"));
        return po;
    }

    private void checkIsTargetConsumer(Integer userId, String targetCustomer,Integer businessId) throws BusinessException {
        //1.basicApi 查询当前userId所属的人群 包含关系判断一下是否有targetCustomer
        List<Integer> userGroupIds = basicApi.getUserGroupIds(userId, businessId);
        //2.判断包含关系
        Integer targetGroupId=Integer.valueOf(targetCustomer);
        Asserts.isTrue(CollectionUtils.isEmpty(userGroupIds),new BusinessException("-2","用户所属人群为空"));
        Asserts.isTrue(!userGroupIds.contains(targetGroupId),new BusinessException("-2","用户不属于该活动目标人群"));
    }

    private ActivityPO checkAndGetActivity(Integer activityId) throws BusinessException {
        //1.查询活动详情
        ActivityPO po = activityRepository.getById(activityId);
        //2.断言空 活动不能为空
        Asserts.isTrue(po==null,new BusinessException("-2","活动不存在"));
        //3.断言 时间 当前领取优惠券时间 必须在活动范围之内 starteDate endDate
        Date now=new Date();
        //不能小于开始时间 now before startDate
        Asserts.isTrue(now.before(po.getStartDate()),new BusinessException("-2","活动未开始"));
        //不能大于结束时间 now after endDate
        Asserts.isTrue(now.after(po.getEndDate()),new BusinessException("-2","活动已结束"));
        return po;
    }

    private Integer checkAndGetUser() throws BusinessException {
        UserPrinciple userPrinciple = AuthenticationUtil.get();
        Asserts.isTrue(userPrinciple== null, new BusinessException("-2","认证解析失败"));
        //当前系统 只有Roles.USER可以领取
        Asserts.isTrue(userPrinciple.getRole()!= Roles.USER,new BusinessException("-2","当前用户角色不符"));
        return userPrinciple.getId();
    }

    public PageResult<UserCouponsVO> userCouponsPage(UserCouponQuery couponQuery) throws BusinessException {
        //1.准备一个分页对象返回
        PageResult<UserCouponsVO> voPage=new PageResult<>(couponQuery);
        voPage.setTotal(100L);
        //2.查询当前用户的优惠券列表 解析认证
        Integer userId=getUserId(); //还需要query里status状态
        couponQuery.setUserId(userId);
        List<UserCouponsPO> pos=getUserCoupons(couponQuery);
        List<UserCouponsVO> vos=null;
        if (CollectionUtils.isNotEmpty(pos)){
            vos=pos.stream().map(po->{
                UserCouponsVO vo=new UserCouponsVO();
                BeanUtils.copyProperties(po,vo);
                return vo;
            }).collect(Collectors.toList());
        }
        voPage.setObjects(vos);
        return voPage;
    }

    private List<UserCouponsPO> getUserCoupons(UserCouponQuery couponQuery) {
        //where user_id= and status=
        QueryWrapper<UserCouponsPO> queryWrapper=new QueryWrapper();
        queryWrapper.eq("user_id",couponQuery.getUserId());
        queryWrapper.eq("status",couponQuery.getStatus());
        return userCouponsRepository.list(queryWrapper);
    }

    private Integer getUserId() throws BusinessException {
        UserPrinciple userPrinciple = AuthenticationUtil.get();
        Asserts.isTrue(userPrinciple== null, new BusinessException("-2","认证解析失败"));
        return userPrinciple.getId();
    }

    public UserCouponsVO userCouponsDetail(UserCouponCodeQuery query) {
        //1.query携带了code编码 查询领取记录中的详情
        UserCouponsPO po=getUserCouponsDetail(query.getCouponCode());
        UserCouponsVO vo=null;
        if (po!=null){
            vo=new UserCouponsVO();
            BeanUtils.copyProperties(po,vo);
            // 拿到二维码 通过缓存 或者attach服务
            String qrUrl=getCouponQrCode(po);
            vo.setCouponUrl(qrUrl);
        }
        return vo;
    }

    private String getCouponQrCode(UserCouponsPO po) {
        log.info("优惠券:{},查询二维码",po.getCouponCode());
        //引入缓存 如果缓存命中 直接使用缓存url 没命中 调用图片生成url 保存缓存
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        String codeKey="coupon:code:url:"+po.getId();
        String urlCache = opsForValue.get(codeKey);
        String url=null;
        if (StringUtils.isNotEmpty(urlCache)){
            log.info("缓存命中,url:{}",urlCache);
            url=urlCache;
        }else{
            //远程调用attach
            log.info("缓存没命中,调用图片生成");
            AttachQRParam qrParam=new AttachQRParam();
            qrParam.setBusinessType(800);//某个优惠券码 的二维码类型图片
            qrParam.setBusinessId(po.getId());
            qrParam.setContent("https://www.baidu.com?"+po.getCouponCode());
            AttachQrDTO attachQrDTO = attachApi.generateQrCode(qrParam);
            url=attachQrDTO.getUrl();
            //在缓存保存一份,供后续访问使用
            opsForValue.set(codeKey,url);
        }

        return url;
    }

    private UserCouponsPO getUserCouponsDetail(String couponCode) {
        //where coupon_code=#{}
        QueryWrapper<UserCouponsPO> queryWrapper=new QueryWrapper();
        queryWrapper.eq("coupon_code",couponCode);
        return userCouponsRepository.getOne(queryWrapper);
    }
}
